<?php

declare(strict_types=1);
/**
 * This file is part of MoChat.
 * @link     https://mo.chat
 * @document https://mochat.wiki
 * @contact  group@mo.chat
 * @license  https://github.com/mochat-cloud/mochat/blob/master/LICENSE
 */
namespace MoChat\App\WorkMessage\Fetcher;

use Hyperf\Di\Annotation\Inject;
use Hyperf\Logger\LoggerFactory;
use Hyperf\Utils\Codec\Json;
use MoChat\App\SyncData\Contract\DataFetcherInterface;
use MoChat\App\WorkMessage\Contract\WorkMessageConfigContract;
use MoChat\App\WorkMessage\Contract\WorkMessageContract;
use MoChat\App\WorkMessage\Queue\MessageReceivedRawQueue;
use MoChat\App\WorkMessage\Utils\MessageArchiveFactory;
use MoChat\WeWorkFinanceSDK\Exception\FinanceSDKException;
use MoChat\WeWorkFinanceSDK\Exception\InvalidArgumentException;
use MoChat\WeWorkFinanceSDK\WxFinanceSDK;

class MessageDataFetcher implements DataFetcherInterface
{
    /**
     * 每次获取消息数量.
     */
    public const MESSAGE_LIMIT = 100;

    /**
     * @var int
     */
    protected $corpId;

    /**
     * @var WxFinanceSDK
     */
    protected $sdk;

    /**
     * @Inject
     * @var WorkMessageConfigContract
     */
    protected $messageConfigService;

    /**
     * @var WorkMessageContract
     */
    protected $workMessageService;

    /**
     * @Inject
     * @var MessageReceivedRawQueue
     */
    protected $messageReceivedRaw;

    /**
     * @Inject
     * @var MessageArchiveFactory
     */
    protected $messageArchiveFactory;

    /**
     * @var \Psr\Log\LoggerInterface
     */
    protected $logger;

    /**
     * @Inject
     * @var LoggerFactory
     */
    protected $loggerFactory;

    /**
     * @var MessageDataFetcher[]
     */
    protected static $dataFetchers = [];

    /**
     * @var bool
     */
    protected $debug = false;

    public function __construct($corpId = null)
    {
        if (! empty($corpId)) {
            $this->init((int) $corpId);
        }

        $this->logger = $this->loggerFactory->get('messageArchive');
        $this->debug = config('debug.message_archive');
    }

    public static function get(int $id): MessageDataFetcher
    {
        if (isset(static::$dataFetchers[$id]) && static::$dataFetchers[$id] instanceof MessageDataFetcher) {
            return static::$dataFetchers[$id];
        }

        return static::$dataFetchers[$id] = new MessageDataFetcher($id);
    }

    public function getDataSource(): array
    {
        $dataSources = $this->messageConfigService->getWorkMessageConfigsByDoneStatus(['corp_id']);
        if (empty($dataSources)) {
            return [];
        }

        return array_column($dataSources, 'corpId');
    }

    public function pull(int $limit = self::MESSAGE_LIMIT, $sdk = null): array
    {
        $messages = $this->getDecryptChatData($this->getLastSeq(), $limit, 0, $sdk);

        if (empty($messages)) {
            return $messages;
        }

        return $messages;
    }

    public function syncData(array $data)
    {
        if (! empty($data)) {
            $this->messageReceivedRaw->handle($this->corpId, $data);
        }
    }

    /**
     * 获取会话解密记录数据.
     * @param int $seq 起始位置
     * @param int $limit 限制条数
     * @param int $retry 重试次数
     * @param WxFinanceSDK $sdk
     * @return array ...
     * @throws InvalidArgumentException
     * @throws FinanceSDKException
     */
    public function getDecryptChatData(int $seq, int $limit, int $retry = 0, $sdk = null): array
    {
        if ($sdk !== null && $sdk instanceof WxFinanceSDK) {
            $messageSdk = $sdk;
        } else {
            $messageSdk = $this->sdk;
        }

        $config = $messageSdk->getConfig();
        if (! isset($config['private_keys'])) {
            $this->logger->error('缺少配置:private_keys[{"version":"private_key"}]');
            throw new InvalidArgumentException('缺少配置:private_keys[{"version":"private_key"}]');
        }

        $privateKeys = $config['private_keys'];

        try {
            $chatData = json_decode($messageSdk->getChatData($seq, $limit), true)['chatdata'];

            if ($this->debug) {
                $this->logger->debug(sprintf('拉取企业微信会话存档消息成功，消息内容为：%s', Json::encode($chatData)));
            }

            $newChatData = [];
            $lastSeq = $seq;
            foreach ($chatData as $i => $item) {
                $lastSeq = $item['seq'];
                if (! isset($privateKeys[$item['publickey_ver']])) {
                    continue;
                }

                $decryptRandKey = null;
                openssl_private_decrypt(
                    base64_decode($item['encrypt_random_key']),
                    $decryptRandKey,
                    $privateKeys[$item['publickey_ver']],
                    OPENSSL_PKCS1_PADDING
                );

                // TODO 无法解密，一般为公钥不匹配
                // 临时补丁方案，需要改为支持多版本key
                if ($decryptRandKey === null) {
                    $this->logger->error(sprintf('解密消息失败，消息内容：%s, 配置信息为：%s', Json::encode($item), Json::encode($config)));
                    continue;
                }

                $newChatData[$i] = json_decode($messageSdk->decryptData($decryptRandKey, $item['encrypt_chat_msg']), true);
                $newChatData[$i]['seq'] = $item['seq'];
            }

            $this->setLastSeq($lastSeq);

            // 不为空且需要重试
            if (! empty($chatData) && $retry && $retry < 100) {
                return $this->getDecryptChatData($lastSeq, $limit, ++$retry, $sdk);
            }

            return $newChatData;
        } catch (\Exception $e) {
            throw new FinanceSDKException($e->getMessage(), $e->getCode());
        }
    }

    private function init(int $corpId)
    {
        $this->corpId = $corpId;
//        $this->sdk = $this->messageArchiveFactory->get($corpId);
        $this->workMessageService = make(WorkMessageContract::class, [$corpId]);
    }

    private function setLastSeq(int $seq)
    {
        $this->workMessageService->updateWorkMessageLastSeqByCorpId($this->corpId, $seq);
        return $this;
    }

    private function getLastSeq()
    {
        return $this->workMessageService->getWorkMessageLastSeqByCorpId($this->corpId);
    }

    private function getMaxSeq(array $data): int
    {
        return (int) max(array_column($data, 'seq'));
    }
}
